import { assert } from 'chai';

import { NO_ADDITIONAL_NODES_PRESET } from '../../../../src/options/presets/NoCustomNodes';

import { IdentifierNamesGenerator } from '../../../../src/enums/generators/identifier-names-generators/IdentifierNamesGenerator';

import { getRegExpMatch } from '../../../helpers/getRegExpMatch';
import { readFileAsString } from '../../../helpers/readFileAsString';

import { JavaScriptObfuscator } from '../../../../src/JavaScriptObfuscatorFacade';

describe('DeadCodeInjectionTransformer', () => {
    const variableMatch: string = '_0x([a-f0-9]){4,6}';
    const hexMatch: string = '0x[a-f0-9]';
    const stringArrayCallMatch: string = `${variableMatch}\\(${hexMatch}\\)`;

    describe('transformNode', function () {
        this.timeout(100000);

        describe('Variant #1 - 5 simple block statements', () => {
            const regExp: RegExp = new RegExp(
                `if *\\(${variableMatch}\\(${hexMatch}\\) *[=|!]== *${variableMatch}\\(${hexMatch}\\)\\) *\\{` +
                    `(?:console|${variableMatch})\\[${variableMatch}\\(${hexMatch}\\)\\]\\(${variableMatch}\\(${hexMatch}\\)\\);` +
                    `\\} *else *\\{` +
                    `(?:console|${variableMatch})\\[${variableMatch}\\(${hexMatch}\\)\\]\\(${variableMatch}\\(${hexMatch}\\)\\);` +
                    `\\}`,
                'g'
            );
            const expectedMatchesLength: number = 5;

            let matchesLength: number = 0;

            before(() => {
                const code: string = readFileAsString(__dirname + '/fixtures/input-1.js');

                const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, {
                    ...NO_ADDITIONAL_NODES_PRESET,
                    deadCodeInjection: true,
                    deadCodeInjectionThreshold: 1,
                    stringArray: true,
                    stringArrayThreshold: 1
                }).getObfuscatedCode();
                const matches: RegExpMatchArray = <RegExpMatchArray>obfuscatedCode.match(regExp);

                if (matches) {
                    matchesLength = matches.length;
                }
            });

            it('should replace block statements with condition with original block statements and dead code', () => {
                assert.equal(matchesLength, expectedMatchesLength);
            });
        });

        describe('Variant #2 - block statements count is less than `5`', () => {
            const regexp: RegExp = new RegExp(
                `var ${variableMatch} *= *function *\\(\\) *\\{` +
                    `console\\[${variableMatch}\\(${hexMatch}\\)\\]\\(${variableMatch}\\(${hexMatch}\\)\\);` +
                    `\\};`,
                'g'
            );
            const expectedMatchesLength: number = 4;

            let matchesLength: number = 0;

            before(() => {
                const code: string = readFileAsString(__dirname + '/fixtures/block-statements-min-count.js');

                const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, {
                    ...NO_ADDITIONAL_NODES_PRESET,
                    deadCodeInjection: true,
                    deadCodeInjectionThreshold: 1,
                    stringArray: true,
                    stringArrayThreshold: 1
                }).getObfuscatedCode();
                const matches: RegExpMatchArray = <RegExpMatchArray>obfuscatedCode.match(regexp);

                if (matches) {
                    matchesLength = matches.length;
                }
            });

            it("shouldn't add dead code", () => {
                assert.equal(matchesLength, expectedMatchesLength);
            });
        });

        describe('Variant #3 - deadCodeInjectionThreshold: 0', () => {
            const regexp: RegExp = new RegExp(
                `var ${variableMatch} *= *function *\\(\\) *\\{` +
                    `console\\[${variableMatch}\\(${hexMatch}\\)\\]\\(${variableMatch}\\(${hexMatch}\\)\\);` +
                    `\\};`,
                'g'
            );
            const expectedMatchesLength: number = 5;

            let matchesLength: number = 0;

            before(() => {
                const code: string = readFileAsString(__dirname + '/fixtures/input-1.js');

                const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, {
                    ...NO_ADDITIONAL_NODES_PRESET,
                    deadCodeInjection: true,
                    deadCodeInjectionThreshold: 0,
                    stringArray: true,
                    stringArrayThreshold: 1
                }).getObfuscatedCode();
                const matches: RegExpMatchArray = <RegExpMatchArray>obfuscatedCode.match(regexp);

                if (matches) {
                    matchesLength = matches.length;
                }
            });

            it("shouldn't add dead code", () => {
                assert.equal(matchesLength, expectedMatchesLength);
            });
        });

        describe('Variant #4 - prohibited node inside collected block statement', () => {
            describe('Variant #1 - function declaration in block statement', () => {
                const functionRegExp: RegExp = new RegExp(
                    `var ${variableMatch} *= *function *\\(\\) *\\{` +
                        `console\\[${variableMatch}\\(${hexMatch}\\)\\]\\(${variableMatch}\\(${hexMatch}\\)\\);` +
                        `\\};`,
                    'g'
                );
                const functionDeclarationRegExp: RegExp = new RegExp(
                    `function *${variableMatch} *\\(${variableMatch}\\) *{}`,
                    'g'
                );
                const expectedFunctionMatchesLength: number = 4;
                const expectedFunctionDeclarationMatchesLength: number = 1;

                let functionMatchesLength: number = 0,
                    functionDeclarationMatchesLength: number = 0;

                before(() => {
                    const code: string = readFileAsString(
                        __dirname + '/fixtures/function-declaration-inside-block-statement.js'
                    );

                    const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, {
                        ...NO_ADDITIONAL_NODES_PRESET,
                        deadCodeInjection: true,
                        deadCodeInjectionThreshold: 1,
                        stringArray: true,
                        stringArrayThreshold: 1
                    }).getObfuscatedCode();
                    const functionMatches: RegExpMatchArray = <RegExpMatchArray>obfuscatedCode.match(functionRegExp);
                    const loopMatches: RegExpMatchArray = <RegExpMatchArray>(
                        obfuscatedCode.match(functionDeclarationRegExp)
                    );

                    if (functionMatches) {
                        functionMatchesLength = functionMatches.length;
                    }

                    if (loopMatches) {
                        functionDeclarationMatchesLength = loopMatches.length;
                    }
                });

                it("match #1: shouldn't add dead code", () => {
                    assert.equal(functionMatchesLength, expectedFunctionMatchesLength);
                });

                it("match #2: shouldn't add dead code", () => {
                    assert.equal(functionDeclarationMatchesLength, expectedFunctionDeclarationMatchesLength);
                });
            });

            describe('Variant #2 - break or continue statement in block statement', () => {
                describe('Variant #1', () => {
                    const functionRegExp: RegExp = new RegExp(
                        `var ${variableMatch} *= *function *\\(\\) *\\{` +
                            `console\\[${variableMatch}\\(${hexMatch}\\)\\]\\(${variableMatch}\\(${hexMatch}\\)\\);` +
                            `\\};`,
                        'g'
                    );
                    const loopRegExp: RegExp = new RegExp(
                        `for *\\(var ${variableMatch} *= *${hexMatch}; *${variableMatch} *< *${hexMatch}; *${variableMatch}\\+\\+\\) *\\{` +
                            `(?:continue|break);` +
                            `\\}`,
                        'g'
                    );
                    const expectedFunctionMatchesLength: number = 4;
                    const expectedLoopMatchesLength: number = 2;

                    let functionMatchesLength: number = 0,
                        loopMatchesLength: number = 0;

                    before(() => {
                        const code: string = readFileAsString(__dirname + '/fixtures/break-continue-statement-1.js');

                        const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, {
                            ...NO_ADDITIONAL_NODES_PRESET,
                            deadCodeInjection: true,
                            deadCodeInjectionThreshold: 1,
                            stringArray: true,
                            stringArrayThreshold: 1
                        }).getObfuscatedCode();
                        const functionMatches: RegExpMatchArray = <RegExpMatchArray>(
                            obfuscatedCode.match(functionRegExp)
                        );
                        const loopMatches: RegExpMatchArray = <RegExpMatchArray>obfuscatedCode.match(loopRegExp);

                        if (functionMatches) {
                            functionMatchesLength = functionMatches.length;
                        }

                        if (loopMatches) {
                            loopMatchesLength = loopMatches.length;
                        }
                    });

                    it("match #1: shouldn't add dead code", () => {
                        assert.equal(functionMatchesLength, expectedFunctionMatchesLength);
                    });

                    it("match #2: shouldn't add dead code", () => {
                        assert.equal(loopMatchesLength, expectedLoopMatchesLength);
                    });
                });

                describe('Variant #2', () => {
                    const functionRegExp: RegExp = new RegExp(
                        `var ${variableMatch} *= *function *\\(\\) *\\{` +
                            `console\\[${variableMatch}\\(${hexMatch}\\)\\]\\(${variableMatch}\\(${hexMatch}\\)\\);` +
                            `\\};`,
                        'g'
                    );
                    const loopRegExp: RegExp = new RegExp(
                        `for *\\(var ${variableMatch} *= *${hexMatch}; *${variableMatch} *< *${hexMatch}; *${variableMatch}\\+\\+\\) *` +
                            `(?:continue|break);`,
                        'g'
                    );
                    const expectedFunctionMatchesLength: number = 4;
                    const expectedLoopMatchesLength: number = 2;

                    let functionMatchesLength: number = 0,
                        loopMatchesLength: number = 0;

                    before(() => {
                        const code: string = readFileAsString(__dirname + '/fixtures/break-continue-statement-2.js');

                        const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, {
                            ...NO_ADDITIONAL_NODES_PRESET,
                            deadCodeInjection: true,
                            deadCodeInjectionThreshold: 1,
                            stringArray: true,
                            stringArrayThreshold: 1
                        }).getObfuscatedCode();
                        const functionMatches: RegExpMatchArray = <RegExpMatchArray>(
                            obfuscatedCode.match(functionRegExp)
                        );
                        const loopMatches: RegExpMatchArray = <RegExpMatchArray>obfuscatedCode.match(loopRegExp);

                        if (functionMatches) {
                            functionMatchesLength = functionMatches.length;
                        }

                        if (loopMatches) {
                            loopMatchesLength = loopMatches.length;
                        }
                    });

                    it("match #1: shouldn't add dead code", () => {
                        assert.equal(functionMatchesLength, expectedFunctionMatchesLength);
                    });

                    it("match #2: shouldn't add dead code", () => {
                        assert.equal(loopMatchesLength, expectedLoopMatchesLength);
                    });
                });
            });

            describe('Variant #3 - await expression in block statement', () => {
                const functionRegExp: RegExp = new RegExp(
                    `var ${variableMatch} *= *function *\\(\\) *\\{` +
                        `console\\[${variableMatch}\\(${hexMatch}\\)\\]\\(${variableMatch}\\(${hexMatch}\\)\\);` +
                        `\\};`,
                    'g'
                );
                const awaitExpressionRegExp: RegExp = new RegExp(`await *${variableMatch}\\(\\)`, 'g');
                const expectedFunctionMatchesLength: number = 4;
                const expectedAwaitExpressionMatchesLength: number = 1;

                let functionMatchesLength: number = 0,
                    awaitExpressionMatchesLength: number = 0;

                before(() => {
                    const code: string = readFileAsString(__dirname + '/fixtures/await-expression.js');

                    const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, {
                        ...NO_ADDITIONAL_NODES_PRESET,
                        deadCodeInjection: true,
                        deadCodeInjectionThreshold: 1,
                        stringArray: true,
                        stringArrayThreshold: 1
                    }).getObfuscatedCode();
                    const functionMatches: RegExpMatchArray = <RegExpMatchArray>obfuscatedCode.match(functionRegExp);
                    const awaitExpressionMatches: RegExpMatchArray = <RegExpMatchArray>(
                        obfuscatedCode.match(awaitExpressionRegExp)
                    );

                    if (functionMatches) {
                        functionMatchesLength = functionMatches.length;
                    }

                    if (awaitExpressionMatches) {
                        awaitExpressionMatchesLength = awaitExpressionMatches.length;
                    }
                });

                it("match #1: shouldn't add dead code", () => {
                    assert.equal(functionMatchesLength, expectedFunctionMatchesLength);
                });

                it("match #2: shouldn't add dead code", () => {
                    assert.equal(awaitExpressionMatchesLength, expectedAwaitExpressionMatchesLength);
                });
            });

            describe('Variant #4 - yield expression in block statement', () => {
                const functionRegExp: RegExp = new RegExp(
                    `var ${variableMatch} *= *function *\\(\\) *\\{` +
                        `console\\[${variableMatch}\\(${hexMatch}\\)\\]\\(${variableMatch}\\(${hexMatch}\\)\\);` +
                        `\\};`,
                    'g'
                );
                const yieldExpressionRegExp: RegExp = new RegExp(`yield *${variableMatch}\\(\\)`, 'g');
                const expectedFunctionMatchesLength: number = 4;
                const expectedAwaitExpressionMatchesLength: number = 1;

                let functionMatchesLength: number = 0,
                    yieldExpressionMatchesLength: number = 0;

                before(() => {
                    const code: string = readFileAsString(__dirname + '/fixtures/yield-expression.js');

                    const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, {
                        ...NO_ADDITIONAL_NODES_PRESET,
                        deadCodeInjection: true,
                        deadCodeInjectionThreshold: 1,
                        stringArray: true,
                        stringArrayThreshold: 1
                    }).getObfuscatedCode();
                    const functionMatches: RegExpMatchArray = <RegExpMatchArray>obfuscatedCode.match(functionRegExp);
                    const yieldExpressionMatches: RegExpMatchArray = <RegExpMatchArray>(
                        obfuscatedCode.match(yieldExpressionRegExp)
                    );

                    if (functionMatches) {
                        functionMatchesLength = functionMatches.length;
                    }

                    if (yieldExpressionMatches) {
                        yieldExpressionMatchesLength = yieldExpressionMatches.length;
                    }
                });

                it("match #1: shouldn't add dead code", () => {
                    assert.equal(functionMatchesLength, expectedFunctionMatchesLength);
                });

                it("match #2: shouldn't add dead code", () => {
                    assert.equal(yieldExpressionMatchesLength, expectedAwaitExpressionMatchesLength);
                });
            });

            describe('Variant #5 - super expression in block statement', () => {
                const functionRegExp: RegExp = new RegExp(
                    `var ${variableMatch} *= *function *\\(\\) *\\{` +
                        `console\\[${variableMatch}\\(${hexMatch}\\)\\]\\(${variableMatch}\\(${hexMatch}\\)\\);` +
                        `\\};`,
                    'g'
                );
                const superExpressionRegExp: RegExp = new RegExp(`super *\\(\\);`, 'g');
                const expectedFunctionMatchesLength: number = 4;
                const expectedSuperExpressionMatchesLength: number = 1;

                let functionMatchesLength: number = 0,
                    superExpressionMatchesLength: number = 0;

                before(() => {
                    const code: string = readFileAsString(__dirname + '/fixtures/super-expression.js');

                    const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, {
                        ...NO_ADDITIONAL_NODES_PRESET,
                        deadCodeInjection: true,
                        deadCodeInjectionThreshold: 1,
                        stringArray: true,
                        stringArrayThreshold: 1
                    }).getObfuscatedCode();
                    const functionMatches: RegExpMatchArray = <RegExpMatchArray>obfuscatedCode.match(functionRegExp);
                    const superExpressionMatches: RegExpMatchArray = <RegExpMatchArray>(
                        obfuscatedCode.match(superExpressionRegExp)
                    );

                    if (functionMatches) {
                        functionMatchesLength = functionMatches.length;
                    }

                    if (superExpressionMatches) {
                        superExpressionMatchesLength = superExpressionMatches.length;
                    }
                });

                it("match #1: shouldn't add dead code", () => {
                    assert.equal(functionMatchesLength, expectedFunctionMatchesLength);
                });

                it("match #2: shouldn't add dead code", () => {
                    assert.equal(superExpressionMatchesLength, expectedSuperExpressionMatchesLength);
                });
            });

            describe('Variant #6 - for-await expression in block statement', () => {
                const functionRegExp: RegExp = new RegExp(
                    `var ${variableMatch} *= *function *\\(\\) *\\{` +
                        `console\\[${variableMatch}\\(${hexMatch}\\)\\]\\(${variableMatch}\\(${hexMatch}\\)\\);` +
                        `\\};`,
                    'g'
                );
                const awaitExpressionRegExp: RegExp = new RegExp(
                    `for await *\\(const ${variableMatch} of *\\[]\\){}`,
                    'g'
                );
                const expectedFunctionMatchesLength: number = 4;
                const expectedAwaitExpressionMatchesLength: number = 1;

                let functionMatchesLength: number = 0,
                    awaitExpressionMatchesLength: number = 0;

                before(() => {
                    const code: string = readFileAsString(__dirname + '/fixtures/for-await-expression.js');

                    const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, {
                        ...NO_ADDITIONAL_NODES_PRESET,
                        deadCodeInjection: true,
                        deadCodeInjectionThreshold: 1,
                        stringArray: true,
                        stringArrayThreshold: 1
                    }).getObfuscatedCode();
                    const functionMatches: RegExpMatchArray = <RegExpMatchArray>obfuscatedCode.match(functionRegExp);
                    const awaitExpressionMatches: RegExpMatchArray = <RegExpMatchArray>(
                        obfuscatedCode.match(awaitExpressionRegExp)
                    );

                    if (functionMatches) {
                        functionMatchesLength = functionMatches.length;
                    }

                    if (awaitExpressionMatches) {
                        awaitExpressionMatchesLength = awaitExpressionMatches.length;
                    }
                });

                it("match #1: shouldn't add dead code", () => {
                    assert.equal(functionMatchesLength, expectedFunctionMatchesLength);
                });

                it("match #2: shouldn't add dead code", () => {
                    assert.equal(awaitExpressionMatchesLength, expectedAwaitExpressionMatchesLength);
                });
            });

            describe('Variant #7 - private identifier in block statement', () => {
                const functionRegExp: RegExp = new RegExp(
                    `var ${variableMatch} *= *function *\\(\\) *\\{` +
                        `console\\[${variableMatch}\\(${hexMatch}\\)\\]\\(${variableMatch}\\(${hexMatch}\\)\\);` +
                        `\\};`,
                    'g'
                );
                const privateIdentifierRegExp: RegExp = new RegExp(`this\.#private *= *0x1;`, 'g');
                const expectedFunctionMatchesLength: number = 4;
                const expectedPrivateIdentifierMatchesLength: number = 1;

                let functionMatchesLength: number = 0,
                    privateIdentifierMatchesLength: number = 0;

                before(() => {
                    const code: string = readFileAsString(__dirname + '/fixtures/private-identifier.js');

                    const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, {
                        ...NO_ADDITIONAL_NODES_PRESET,
                        deadCodeInjection: true,
                        deadCodeInjectionThreshold: 1,
                        stringArray: true,
                        stringArrayThreshold: 1
                    }).getObfuscatedCode();
                    const functionMatches: RegExpMatchArray = <RegExpMatchArray>obfuscatedCode.match(functionRegExp);
                    const privateIdentifierMatches: RegExpMatchArray = <RegExpMatchArray>(
                        obfuscatedCode.match(privateIdentifierRegExp)
                    );

                    if (functionMatches) {
                        functionMatchesLength = functionMatches.length;
                    }

                    if (privateIdentifierMatches) {
                        privateIdentifierMatchesLength = privateIdentifierMatches.length;
                    }
                });

                it("match #1: shouldn't add dead code", () => {
                    assert.equal(functionMatchesLength, expectedFunctionMatchesLength);
                });

                it("match #2: shouldn't add dead code", () => {
                    assert.equal(privateIdentifierMatchesLength, expectedPrivateIdentifierMatchesLength);
                });
            });
        });

        describe('Variant #5 - chance of `IfStatement` variant', () => {
            const samplesCount: number = 1000;
            const delta: number = 0.1;
            const expectedDistribution: number = 0.25;

            const ifMatch: string = `if *\\(!!\\[\\]\\) *\\{`;
            const functionMatch: string = `var ${variableMatch} *= *function *\\(\\) *\\{`;

            const match1: string =
                `` +
                `if *\\(${stringArrayCallMatch} *=== *${stringArrayCallMatch}\\) *\\{` +
                `console\\[${stringArrayCallMatch}]\\(${stringArrayCallMatch}\\);` +
                `\\} *else *\\{` +
                `${variableMatch}\\(${stringArrayCallMatch}\\);` +
                `\\}` +
                ``;
            const match2: string =
                `` +
                `if *\\(${stringArrayCallMatch} *!== *${stringArrayCallMatch}\\) *\\{` +
                `console\\[${stringArrayCallMatch}]\\(${stringArrayCallMatch}\\);` +
                `\\} *else *\\{` +
                `${variableMatch}\\(${stringArrayCallMatch}\\);` +
                `\\}` +
                ``;
            const match3: string =
                `` +
                `if *\\(${stringArrayCallMatch} *=== *${stringArrayCallMatch}\\) *\\{` +
                `${variableMatch}\\(${stringArrayCallMatch}\\);` +
                `\\} *else *\\{` +
                `console\\[${stringArrayCallMatch}]\\(${stringArrayCallMatch}\\);` +
                `\\}` +
                ``;
            const match4: string =
                `` +
                `if *\\(${stringArrayCallMatch} *!== *${stringArrayCallMatch}\\) *\\{` +
                `${variableMatch}\\(${stringArrayCallMatch}\\);` +
                `\\} *else *\\{` +
                `console\\[${stringArrayCallMatch}]\\(${stringArrayCallMatch}\\);` +
                `\\}` +
                ``;

            let distribution1: number = 0,
                distribution2: number = 0,
                distribution3: number = 0,
                distribution4: number = 0;

            before(() => {
                const code: string = readFileAsString(__dirname + '/fixtures/if-statement-variants-distribution.js');

                const regExp1: RegExp = new RegExp(`${ifMatch}${functionMatch}${match1}`);
                const regExp2: RegExp = new RegExp(`${ifMatch}${functionMatch}${match2}`);
                const regExp3: RegExp = new RegExp(`${ifMatch}${functionMatch}${match3}`);
                const regExp4: RegExp = new RegExp(`${ifMatch}${functionMatch}${match4}`);

                let count1: number = 0;
                let count2: number = 0;
                let count3: number = 0;
                let count4: number = 0;

                for (let i = 0; i < samplesCount; i++) {
                    const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, {
                        ...NO_ADDITIONAL_NODES_PRESET,
                        deadCodeInjection: true,
                        deadCodeInjectionThreshold: 1,
                        stringArray: true,
                        stringArrayThreshold: 1
                    }).getObfuscatedCode();

                    if (regExp1.test(obfuscatedCode)) {
                        count1++;
                    } else if (regExp2.test(obfuscatedCode)) {
                        count2++;
                    } else if (regExp3.test(obfuscatedCode)) {
                        count3++;
                    } else if (regExp4.test(obfuscatedCode)) {
                        count4++;
                    }
                }

                distribution1 = count1 / samplesCount;
                distribution2 = count2 / samplesCount;
                distribution3 = count3 / samplesCount;
                distribution4 = count4 / samplesCount;
            });

            it('Variant #1: `IfStatement` variant should have distribution close to `0.25`', () => {
                assert.closeTo(distribution1, expectedDistribution, delta);
            });

            it('Variant #2: `IfStatement` variant should have distribution close to `0.25`', () => {
                assert.closeTo(distribution2, expectedDistribution, delta);
            });

            it('Variant #3: `IfStatement` variant should have distribution close to `0.25`', () => {
                assert.closeTo(distribution3, expectedDistribution, delta);
            });

            it('Variant #4: `IfStatement` variant should have distribution close to `0.25`', () => {
                assert.closeTo(distribution4, expectedDistribution, delta);
            });
        });

        describe('Variant #6 - block scope of block statement is `ProgramNode`', () => {
            const regExp: RegExp = new RegExp(
                `if *\\(!!\\[\\]\\) *{` +
                    `console\\[${variableMatch}\\(${hexMatch}\\)\\]\\(${variableMatch}\\(${hexMatch}\\)\\);` +
                    `\\}`
            );

            let obfuscatedCode: string;

            before(() => {
                const code: string = readFileAsString(__dirname + '/fixtures/block-scope-is-program-node.js');

                obfuscatedCode = JavaScriptObfuscator.obfuscate(code, {
                    ...NO_ADDITIONAL_NODES_PRESET,
                    stringArray: true,
                    stringArrayThreshold: 1,
                    deadCodeInjection: true,
                    deadCodeInjectionThreshold: 1
                }).getObfuscatedCode();
            });

            it("shouldn't add dead code in block statements with `ProgramNode` block scope", () => {
                assert.match(obfuscatedCode, regExp);
            });
        });

        describe('Variant #7 - correct obfuscation of dead-code block statements', () => {
            const variableName: string = 'importantVariableName';

            let obfuscatedCode: string;

            before(() => {
                const code: string = readFileAsString(
                    __dirname + '/fixtures/obfuscation-of-dead-code-block-statements.js'
                );

                obfuscatedCode = JavaScriptObfuscator.obfuscate(code, {
                    ...NO_ADDITIONAL_NODES_PRESET,
                    deadCodeInjection: true,
                    deadCodeInjectionThreshold: 1,
                    debugProtection: true
                }).getObfuscatedCode();
            });

            it('should correctly obfuscate dead-code block statements and prevent any exposing of internal variable names', () => {
                assert.notInclude(obfuscatedCode, variableName);
            });
        });

        describe('Variant #8 - unique names for dead code identifiers', () => {
            /**
             * Code:
             *
             * (function(variable){
             *   function foo () {
             *      return variable.push(1);
             *   }
             *
             *   function bar () {
             *      var variable = 1;
             *   }
             *
             *   function baz() {
             *      var variable = 2;
             *   }
             *
             *   function bark() {
             *      var variable = 3;
             *   }
             *
             *   function hawk() {
             *      var variable = 4;
             *   }
             * })([]);
             *
             * With this code, dead code can be added to the first function `foo`
             * If dead code won't be renamed before add - identifier name inside dead code block statement can be
             * the same as identifier name inside transformed block statement:
             *
             * (function(variable){
             *   function foo () {
             *      if (1 !== 1) {
             *          var variable = 1; // <- overwriting value of function parameter
             *      } else {
             *          return variable.push(1);
             *      }
             *   }
             *
             *   function bar () {
             *      var variable = 1;
             *   }
             *
             *   function baz() {
             *      var variable = 2;
             *   }
             *
             *   function bark() {
             *      var variable = 3;
             *   }
             *
             *   function hawk() {
             *      var variable = 4;
             *   }
             * })([]);
             *
             * So, added dead code variable declaration will overwrite a value of function parameter.
             * This should never happen.
             */
            describe('Variant #1', () => {
                const functionParameterMatch: string = `` + `\\(function\\((\\w)\\){` + ``;
                const deadCodeMatch: string =
                    `` +
                    `function \\w *\\(\\w\\) *{` +
                    `if *\\(.{0,30}\\) *{` +
                    `var (\\w).*?;` +
                    `} *else *{` +
                    `return *(\\w).*?;` +
                    `}` +
                    `}` +
                    ``;
                const functionParameterRegExp: RegExp = new RegExp(functionParameterMatch);
                const deadCodeRegExp: RegExp = new RegExp(deadCodeMatch);

                let result: boolean = false,
                    functionIdentifierName: string | null,
                    returnIdentifierName: string | null,
                    variableDeclarationIdentifierName: string | null,
                    obfuscatedCode: string;

                before(() => {
                    const code: string = readFileAsString(
                        __dirname + '/fixtures/unique-names-for-dead-code-identifiers.js'
                    );

                    for (let i: number = 0; i < 100; i++) {
                        while (true) {
                            try {
                                obfuscatedCode = JavaScriptObfuscator.obfuscate(code, {
                                    ...NO_ADDITIONAL_NODES_PRESET,
                                    deadCodeInjection: true,
                                    deadCodeInjectionThreshold: 1,
                                    identifierNamesGenerator: IdentifierNamesGenerator.MangledIdentifierNamesGenerator
                                }).getObfuscatedCode();
                                functionIdentifierName = getRegExpMatch(obfuscatedCode, functionParameterRegExp, 0);
                                variableDeclarationIdentifierName = getRegExpMatch(obfuscatedCode, deadCodeRegExp, 0);
                                returnIdentifierName = getRegExpMatch(obfuscatedCode, deadCodeRegExp, 1);
                                break;
                            } catch {}
                        }

                        if (
                            // variable declaration from dead code is affects original code
                            functionIdentifierName === variableDeclarationIdentifierName &&
                            returnIdentifierName === variableDeclarationIdentifierName
                        ) {
                            result = false;
                            break;
                        }

                        result = true;
                    }
                });

                it('should generate separate identifiers for common AST and dead code', () => {
                    assert.isOk(result, 'wrong identifier names');
                });
            });

            describe('Variant #2', () => {
                const functionParameterMatch: string = `` + `\\(function\\((\\w)\\){` + ``;
                const deadCodeMatch: string =
                    `` +
                    `function \\w *\\(\\w\\) *{` +
                    `if *\\(.{0,30}\\) *{` +
                    `return *(\\w).{0,40};` +
                    `} *else *{` +
                    `var (\\w).*?;` +
                    `}` +
                    `}` +
                    ``;
                const functionParameterRegExp: RegExp = new RegExp(functionParameterMatch);
                const deadCodeRegExp: RegExp = new RegExp(deadCodeMatch);

                let result: boolean = false,
                    functionIdentifierName: string | null,
                    returnIdentifierName: string | null,
                    variableDeclarationIdentifierName: string | null,
                    obfuscatedCode: string;

                before(() => {
                    const code: string = readFileAsString(
                        __dirname + '/fixtures/unique-names-for-dead-code-identifiers.js'
                    );

                    for (let i: number = 0; i < 100; i++) {
                        while (true) {
                            try {
                                obfuscatedCode = JavaScriptObfuscator.obfuscate(code, {
                                    ...NO_ADDITIONAL_NODES_PRESET,
                                    deadCodeInjection: true,
                                    deadCodeInjectionThreshold: 1,
                                    identifierNamesGenerator: IdentifierNamesGenerator.MangledIdentifierNamesGenerator
                                }).getObfuscatedCode();
                                functionIdentifierName = getRegExpMatch(obfuscatedCode, functionParameterRegExp, 0);
                                returnIdentifierName = getRegExpMatch(obfuscatedCode, deadCodeRegExp, 0);
                                variableDeclarationIdentifierName = getRegExpMatch(obfuscatedCode, deadCodeRegExp, 1);
                                break;
                            } catch {}
                        }

                        if (
                            // variable declaration from dead code is affects original code
                            functionIdentifierName === variableDeclarationIdentifierName &&
                            returnIdentifierName === variableDeclarationIdentifierName
                        ) {
                            console.log(obfuscatedCode);
                            result = false;
                            break;
                        }

                        result = true;
                    }
                });

                it('should generate separate identifiers for common AST and dead code', () => {
                    assert.isOk(result, 'wrong identifier names');
                });
            });
        });

        describe('Variant #9 - block statements with empty body', () => {
            const regExp: RegExp = new RegExp(
                `function *${variableMatch} *\\(\\) *{ *} *` + `${variableMatch} *\\(\\); *`,
                'g'
            );
            const expectedMatchesLength: number = 5;

            let matchesLength: number = 0;

            before(() => {
                const code: string = readFileAsString(__dirname + '/fixtures/block-statement-empty-body.js');

                const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, {
                    ...NO_ADDITIONAL_NODES_PRESET,
                    stringArray: true,
                    stringArrayThreshold: 1,
                    deadCodeInjection: true,
                    deadCodeInjectionThreshold: 1
                }).getObfuscatedCode();

                const functionMatches: RegExpMatchArray = <RegExpMatchArray>obfuscatedCode.match(regExp);

                if (functionMatches) {
                    matchesLength = functionMatches.length;
                }
            });

            it("shouldn't add dead code conditions to the block empty block statements", () => {
                assert.isAtLeast(matchesLength, expectedMatchesLength);
            });
        });

        describe('Variant #10 - block statement with scope-hoisting', () => {
            describe('Variant #1: collecting of block statements', () => {
                const regExp: RegExp = new RegExp(
                    `${variableMatch} *\\(\\); *` +
                        `var ${variableMatch} *= *0x2; *` +
                        `function *${variableMatch} *\\(\\) *{ *} *`,
                    'g'
                );
                const expectedMatchesLength: number = 5;

                let matchesLength: number = 0;

                before(() => {
                    const code: string = readFileAsString(
                        __dirname + '/fixtures/block-statement-with-scope-hoisting-1.js'
                    );

                    const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, {
                        ...NO_ADDITIONAL_NODES_PRESET,
                        stringArray: true,
                        stringArrayThreshold: 1,
                        deadCodeInjection: true,
                        deadCodeInjectionThreshold: 1
                    }).getObfuscatedCode();

                    const functionMatches: RegExpMatchArray = <RegExpMatchArray>obfuscatedCode.match(regExp);

                    if (functionMatches) {
                        matchesLength = functionMatches.length;
                    }
                });

                it("shouldn't collect block statements with scope-hoisting", () => {
                    assert.equal(matchesLength, expectedMatchesLength);
                });
            });

            describe('Variant #2: wrapping of block statements in dead code conditions', () => {
                const regExp: RegExp = new RegExp(
                    `function *${variableMatch} *\\(\\) *{ *` +
                        `var ${variableMatch} *= *0x1; *` +
                        `${variableMatch} *\\(\\); *` +
                        `var ${variableMatch} *= *0x2; *` +
                        `function *${variableMatch} *\\(\\) *{ *} *` +
                        `var ${variableMatch} *= *0x3; *` +
                        `}`,
                    'g'
                );

                let obfuscatedCode: string;

                before(() => {
                    const code: string = readFileAsString(
                        __dirname + '/fixtures/block-statement-with-scope-hoisting-2.js'
                    );

                    obfuscatedCode = JavaScriptObfuscator.obfuscate(code, {
                        ...NO_ADDITIONAL_NODES_PRESET,
                        stringArray: true,
                        stringArrayThreshold: 1,
                        deadCodeInjection: true,
                        deadCodeInjectionThreshold: 1
                    }).getObfuscatedCode();
                });

                it("shouldn't wrap block statements in dead code conditions", () => {
                    assert.match(obfuscatedCode, regExp);
                });
            });
        });

        describe('Variant #11 - prevailing kind of variables of inserted code', () => {
            describe('Variant #1: base', () => {
                const variableDeclarationsRegExp: RegExp = new RegExp(
                    `const ${variableMatch} *= *\\[\\]; *` + `var ${variableMatch} *= *\\[\\]; *`,
                    'g'
                );
                const invalidVariableDeclarationsRegExp: RegExp = new RegExp(
                    `var ${variableMatch} *= *\\[\\]; *` + `var ${variableMatch} *= *\\[\\]; *`,
                    'g'
                );

                const forLoopRegExp: RegExp = new RegExp(
                    `for *\\(const ${variableMatch} of ${variableMatch}\\) *{`,
                    'g'
                );
                const invalidForLoopRegExp: RegExp = new RegExp(
                    `for *\\(var ${variableMatch} of ${variableMatch}\\) *{`,
                    'g'
                );

                let obfuscatedCode: string;

                before(() => {
                    const code: string = readFileAsString(__dirname + '/fixtures/prevailing-kind-of-variables-1.js');

                    obfuscatedCode = JavaScriptObfuscator.obfuscate(code, {
                        ...NO_ADDITIONAL_NODES_PRESET,
                        deadCodeInjection: true,
                        deadCodeInjectionThreshold: 1
                    }).getObfuscatedCode();
                });

                it("Match #1: shouldn't replace kinds of variables of inserted original code", () => {
                    assert.match(obfuscatedCode, variableDeclarationsRegExp);
                });

                it("Match #2: shouldn't replace kinds of variables of inserted original code", () => {
                    assert.notMatch(obfuscatedCode, invalidVariableDeclarationsRegExp);
                });

                it("Match #3: shouldn't replace kinds of variables of inserted original code", () => {
                    assert.match(obfuscatedCode, forLoopRegExp);
                });

                it("Match #4: shouldn't replace kinds of variables of inserted original code", () => {
                    assert.notMatch(obfuscatedCode, invalidForLoopRegExp);
                });
            });
        });

        describe('Variant #12 - correct integration with `stringArrayWrappersChainedCalls` option', () => {
            const regExp: RegExp = new RegExp(
                `var ${variableMatch} *= *${variableMatch}; *` +
                    `if *\\(${variableMatch}\\(${hexMatch}\\) *[=|!]== *${variableMatch}\\(${hexMatch}\\)\\) *\\{` +
                    `(?:console|${variableMatch})\\[${variableMatch}\\(${hexMatch}\\)\\]\\(${variableMatch}\\(${hexMatch}\\)\\);` +
                    `\\} *else *\\{` +
                    `(?:console|${variableMatch})\\[${variableMatch}\\(${hexMatch}\\)\\]\\(${variableMatch}\\(${hexMatch}\\)\\);` +
                    `\\}`,
                'g'
            );
            const expectedMatchesLength: number = 5;

            let matchesLength: number = 0;

            before(() => {
                const code: string = readFileAsString(__dirname + '/fixtures/input-1.js');

                const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, {
                    ...NO_ADDITIONAL_NODES_PRESET,
                    deadCodeInjection: true,
                    deadCodeInjectionThreshold: 1,
                    stringArray: true,
                    stringArrayThreshold: 1,
                    stringArrayWrappersCount: 1,
                    stringArrayWrappersChainedCalls: true
                }).getObfuscatedCode();
                const matches: RegExpMatchArray = <RegExpMatchArray>obfuscatedCode.match(regExp);

                if (matches) {
                    matchesLength = matches.length;
                }
            });

            it('should unwrap dead code injection root AST host node before the string array transformer', () => {
                assert.equal(matchesLength, expectedMatchesLength);
            });
        });

        describe('Variant #13 - correct integration with `EvalCallExpressionTransformer`', () => {
            const evalWithDeadCodeRegExp: RegExp = new RegExp(`eval\\(\'if *\\(${variableMatch}`, 'g');

            let obfuscatedCode: string;

            before(() => {
                const code: string = readFileAsString(
                    __dirname + '/fixtures/eval-call-expression-transformer-integration.js'
                );

                obfuscatedCode = JavaScriptObfuscator.obfuscate(code, {
                    ...NO_ADDITIONAL_NODES_PRESET,
                    deadCodeInjection: true,
                    deadCodeInjectionThreshold: 1,
                    stringArray: true,
                    stringArrayThreshold: 1
                }).getObfuscatedCode();
                console.log(obfuscatedCode);
            });

            it("match #1: shouldn't add dead code to the eval call expression", () => {
                assert.notMatch(obfuscatedCode, evalWithDeadCodeRegExp);
            });
        });
    });
});
